
"""
<p>Refer to http://www.sonicspot.com/guide/midifiles.html</p>
"""

__version__ = "0.21"

__aof__ = ('Musical Instrument Digital Interface', 'MID')
__extension__ = (('MIDI Files', ('*.mdi', '*.midi', '*.smf')),)


def _consolateFieldOjbect(target, title, content='', start=0, size=0,
                          args=None, patch=False):
    if start == 0:
        if args is None:
            args = dict()
        start = args.get('start', 0) + args.get('offset', 0)
        if patch:
            start -= size

    return target.consolidateFieldObject(
        title, content, start=start, size=size)


def _readFileWithUpdate(afile, size, args):
    ret = afile.read(size, args['offset'])
    args['offset'] += size

    return ret


def handleChannelEvent(entry, source, target, tp, channel, size, args):

    def __addDictWithExtraRange(di, ranges, template, base=0):
        for k in ranges:
            if isinstance(k, (list, tuple)):
                base = __addDictWithExtraRange(di, k, template, base)
            else:
                value = template.replace('##', '%d' % base)
                di[k] = value
                base += 1

        return base

    _EVENT_TYPE = {
        0x08: 'Note Off',          0x09: 'Note On',
        0x0a: 'Note Aftertouch',   0x0b: 'Controller',
        0x0c: 'Program Change',    0x0d: 'Channel Aftertouch',
        0x0e: 'Pitch Bend'
    }

    entry.appendChildren(_consolateFieldOjbect(target, '0x%x' % tp,
                         _EVENT_TYPE[tp], size=1, args=args, patch=True))
    entry.appendChildren(_consolateFieldOjbect(target, 'Channel', channel,
                         size=1, args=args, patch=True))

    def _controllerType(value):
        _CONTROLLER_TYPE = {
            0x00: 'Bank Select',           0x01: 'Modulation',
            0x02: 'Breath Controller',     0x04: 'Foot Controller',
            0x05: 'Portamento Time',       0x06: 'Data Entry (MSB)',
            0x07: 'Main Volume',           0x08: 'Balance',
            0x0a: 'Pan',                   0x0b: 'Expression Controller',
            0x0c: 'Effect Control 1',      0x0d: 'Effect Control 2',
            0x40: 'Damper pedal (sustain)',
            0x41: 'Portamento',            0x42: 'Sostenuto',
            0x43: 'Soft Pedal',            0x44: 'Legato Footswith',
            0x45: 'Hold 2',
            0x46: 'Sound Controller 1 (default: Timber Variation)',
            0x47: 'Sound Controller 2 (default: Timber/Harmonic Conent)',
            0x48: 'Sound Controller 3 (default: Release Time)',
            0x49: 'Sound Controller 4 (default: Attack Time)',
            0x4a: 'Sound Controller 5',    0x4b: 'Sound Controller 6',
            0x4c: 'Sound Controller 7',    0x4d: 'Sound Controller 8',
            0x4e: 'Sound Controller 9',    0x4f: 'Sound Controller 10',
            0x50: 'General-Purpose Controller 5',
            0x51: 'General-Purpose Controller 6',
            0x52: 'General-Purpose Controller 7',
            0x53: 'General-Purpose Controller 8',
            0x54: 'Portamento Control',
            0x5b: 'Effects 1 Depth (formerly External Effects Depth)',
            0x5c: 'Effects 2 Depth (formerly Tremolo Depth)',
            0x5d: 'Effects 3 Depth (formerly Chorus Depth)',
            0x5e: 'Effects 4 Depth (formerly Celeste Detune)',
            0x5f: 'Effects 5 Depth (formerly Phaser Depth)',
            0x60: 'Data Increment',        0x61: 'Data Decrement',
            0x62: 'Non-Registered Parameter Number (LSB)',
            0x63: 'Non-Registered Parameter Number (MSB)',
            0x64: 'Registered Parameter Number (LSB)',
            0x65: 'Registered Parameter Number (MSB)',
        }

        __addDictWithExtraRange(_CONTROLLER_TYPE, (0x10, 0x13),
                                'General-Purpose Controlers ##', 1)
        __addDictWithExtraRange(_CONTROLLER_TYPE, (0x20, 0x3f),
                                'LSB for controllers ##')
        __addDictWithExtraRange(_CONTROLLER_TYPE,
                                (0x79, 0x7f), 'Mode Messages')

        return '%s (0x%02x)' % (_CONTROLLER_TYPE.get(value, 'Unknonw'), value)

    _ACTION_MAPS = {
        0x08: ('Note Number', 'Velocity'),
        0x09: ('Note Number', 'Velocity'),
        0x0a: ('Note Number', 'Account'),
        0x0b: ({'name': 'Controller Type', 'func': _controllerType}, 'Value'),
        0x0c: ('Program Number', None),
        0x0d: ('Amount', None),
        0x0e: ('Value (LSB)', 'Value (MSB)')
    }

    for action in _ACTION_MAPS[tp]:
        p0 = ord(_readFileWithUpdate(source, 1, args))
        if action:
            if isinstance(action, dict):
                va = action.get('func')(p0)
                entry.appendChildren(
                    _consolateFieldOjbect(
                        target, action.get('name', ''), va, size=1,
                        args=args, patch=True))
            else:
                entry.appendChildren(_consolateFieldOjbect(target, action, p0,
                                     size=1, args=args, patch=True))

    return True


def handleMetaEvent(entry, source, target, size, args):
    _META_TYPE = {
        0x00: 'Sequence Number',       0x01: 'Text Event',
        0x02: 'Copyright Notice',      0x03: 'Sequence/Track Name',
        0x04: 'Instrument Name',       0x05: 'Lyrics',
        0x06: 'Marker',                0x07: 'Cue Point',
        0x20: 'MIDI Channel Prefix',   0x2f: 'End of Track',
        0x51: 'Set Tempo',             0x54: 'SMPTE Offset',
        0x58: 'Time Signature',        0x59: 'Key Signature',
        0x7f: 'Sequence Specific'
    }

    entry.appendChildren(
        _consolateFieldOjbect(
            target, 'MetaEvent',
            '0x%2x' % 0xff, size=1, args=args, patch=True))

    tp = ord(_readFileWithUpdate(source, 1, args))
    entry.appendChildren(
        _consolateFieldOjbect(
            target, 'Type (0x%02x)' % tp,
            _META_TYPE.get(tp, 'Unknown'), size=1, args=args, patch=True))

    var, length = target.readVariableValue(source, **args)
    entry.appendChildren(_consolateFieldOjbect(target, 'Length', length,
                         size=var, args=args))
    args['offset'] += var

    _ACTION_MAPS = {
        0x00: ('Number (MSB)', 'Number (LSB)'),
        0x20: ('Channel',),
        0x54: ('Hour', 'Min', 'Sec', 'Fr', 'SubFr'),
        0x58: ('Number', 'Denom', 'Metro', '32nds'),
        0x59: ('Key', 'Scale')
    }

    if tp in _ACTION_MAPS:
        for action in _ACTION_MAPS[tp]:
            p0 = ord(_readFileWithUpdate(source, 1, args))
            entry.appendChildren(
                _consolateFieldOjbect(target, action, '%d' % p0,
                                      size=1, args=args, patch=True))
    elif 0x01 <= tp <= 0x07:
        p0 = target.readString(source, **args)
        entry.appendChildren(_consolateFieldOjbect(target, 'Text',
                             p0, size=len(p0), args=args))
        # update the event content
        entry.update(content=p0)
        args['offset'] += len(p0)
    elif tp == 0x51:
        p0 = target.readValue(source, 3, **args)
        entry.appendChildren(_consolateFieldOjbect(target, 'Quater-Note',
                             p0, size=3, args=args))
        args['offset'] += 3
    elif tp == 0x7f:
        entry.appendChildren(_consolateFieldOjbect(target, 'Data',
                             length, size=var, args=args))
    elif tp not in _META_TYPE:
        return False

    return True


def handleSysExEvent(entry, source, target, char, size, args):
    objectp = _consolateFieldOjbect(
        target, 'NormalSysExEvents',
        '0x%02x' % char, size=1, args=args, patch=True)
    entry.append(objectp)

    var, length = target.readVariableValue(source, **args)
    entry.appendChildren(_consolateFieldOjbect(target, 'Length',
                         length, size=1, args=args, patch=True))
    args['offset'] += var
    entry.appendChildren(_consolateFieldOjbect(target, 'Data',
                         size=length, args=args))
    ends = source.read(1, args['offset'] + var)
    if char == 0xf0:
        if ends == 0xf7:
            title = 'NormalSysExEvents'
        else:
            title = 'DividedSysExEvents'
    else:
        if ends == 0xf7:
            title = 'DividedSysExEvents'
        else:
            title = 'AuthorizationSysExEvents'

    objectp.update(subject=title)
    return True


# handel the structure below
#   Delta Time   | Event Type Value | MIDI Channel | Parameter 1 | Parameter 2
# ---------------+------------------+--------------+-------------+-------------
# variable-length|     4 bits       |    4 bits    |   1 byte    |  1 byte
def parseEventData(entry, source, target, size, args):
    ret = True

    k, idx = 0, 0
    target.notifyProgress(args, 'Read MIDI event data')
    while k < size and ret:
        offset = args['offset']
        var, delta = target.readVariableValue(source, **args)
        obj = _consolateFieldOjbect(target, 'Event#%d' % idx, args=args)
        obj.appendChildren(_consolateFieldOjbect(target, 'DeltaTime',
                           delta, size=var, args=args))
        args['offset'] += var

        char = ord(_readFileWithUpdate(source, 1, args))
        tp, channel = (char >> 4) & 0x0f, char & 0x0f
        if 0x08 <= tp <= 0x0e:
            ret = handleChannelEvent(obj, source, target, tp,
                                     channel, size, args)
        elif char == 0xff:
            ret = handleMetaEvent(obj, source, target, size, args)
        elif char == 0xf0 or char == 0xf7:
            ret = handleSysExEvent(obj, source, target, char, size, args)

        if ret:
            obj.update(
                field=target.newFieldObject(args['start'] + offset,
                                            args['offset'] - offset))
            entry.appendChildren(obj)
            idx += 1

        k += args['offset'] - offset

    return ret


def parseTracks(entry, source, target, key, args):
    _MTRK_HEADER = (
        ('Magic', 4, 'string,verify', {'verified': 'MTrk'}),
        ('Size', 4, 'decimal,export')
    )

    target.notifyProgress(args, 'Read track header')
    ret, objectp, consumed, _, exported = target.parse(
        'Trunk Header', _MTRK_HEADER, afile=source, **args)

    if ret:
        size = exported[0]
        track = _consolateFieldOjbect(target, 'Track#%d' % key,
                                      size=consumed + size, args=args)
        entry.appendChildren(track)
        track.appendChildren(objectp)

        args['offset'] += consumed
        offset = args['offset']
        if consumed < exported[0]:
            obj = _consolateFieldOjbect(target, 'Events', size=size, args=args)
            track.appendChildren(obj)
            ret = parseEventData(obj, source, target, size, args)

    return ret


def parseMThdHdr(entry, source, target, args):
    _MTHD_HEADER = (
        ('Magic', 4, 'string,verify', {'verified': 'MThd'}),
        ('Size', 4, 'decimal,verify', {'verified': 6}),
        ('MIDI Format', 2, 'value'),
        ('Tracks', 2, 'value,export'),
        ('Division', 2, 'value')
    )

    tracks = -1

    target.notifyProgress(args, 'Read MIDI header')
    ret, objectp, consumed, _, exported = target.parse(
        'Trunk Header', _MTHD_HEADER, afile=source, **args)
    if ret:
        entry.append(objectp)
        args['offset'] += consumed
        tracks = exported[0]

    return ret, tracks


class _MidiDecoder:
    def Execute(self, source, target, notify, **args):
        args['f'] = source
        args['l'] = len(source)
        args['c'] = notify
        args['e'] = False
        args['offset'] = 0

        target.setEndian('big-endian', args)
        entry = target.getEntry()
        ret, tracks = parseMThdHdr(entry, source, target, args)
        if ret:
            if tracks > 0:
                trackEntry = _consolateFieldOjbect(
                    target, 'Tracks',
                    size=args['l'] - args['offset'], args=args)

                for k in xrange(tracks):
                    ret = parseTracks(trackEntry, source, target, k, args)
                    if not ret:
                        break

                entry.append(trackEntry)

        return ret


def fmtService():
    return _MidiDecoder
